Skip to content

淺談 C# Property (屬性) 語法糖與 NRT 機制演進

TLDR

  • field 關鍵字 (C# 14):允許在自動屬性中直接存取編譯器產生的隱藏欄位,無需退回手寫 Backing Field 即可加入邏輯。
  • 避免 =>{ get; } = 混淆=> 為動態計算,每次呼叫都會執行;{ get; } = 為靜態快取,僅在物件初始化時執行一次。
  • NRT 實用性改善:透過 initrequired 關鍵字,解決了 DTO 在 NRT 機制下被迫使用 null! 或無意義預設值的困擾。
  • 強制編譯檢查:透過 <WarningsAsErrors>nullable</WarningsAsErrors> 設定,可將 NRT 警告提升為編譯錯誤,確保團隊合約一致性。
  • 建構子與 required 的協作:使用 [SetsRequiredMembers] 屬性可告知編譯器建構子已完成必要屬性賦值,解決與 required 的衝突。

C# Property (屬性) 的語法演進史

C# 屬性語法經歷了多次演進,旨在簡化定義並減少冗餘代碼。

1. 早期古典寫法 (Backing Field)

在 C# 1.0 時期,屬性必須手動宣告私有欄位來儲存資料。

csharp
public class User {
    private string name;
    
    public string Name {
        get { return name; }
        set { name = value; }
    }
}

2. 自動實作屬性 (Auto-Implemented Properties)

C# 3.0 引入,由編譯器自動產生底層欄位,適用於無需額外邏輯的資料容器。

csharp
public class User {
    public string Name { get; set; }
}

3. 屬性初始值設定項 (Property Initializers)

C# 6.0 允許在自動屬性定義時直接給定初始值。

csharp
public class User {
    public string Name { get; set; } = "Default Name";
}

4. Expression-bodied 屬性

C# 6.0 與 7.0 引入 => 語法,使屬性定義更簡潔。

WARNING

什麼情況下會遇到問題:混淆 Expression-bodied (=>) 與 Property Initializer ({ get; } =) 的執行時機。

  • public string Name => "Default Name":每次讀取時重新計算。
  • public string Name { get; } = "Default Name":僅在物件實體化時執行一次。

錯誤範例

csharp
public class Order {
    // 錯誤:每次讀取都會產生新 Guid,導致序列化或 Log 追蹤異常
    public Guid OrderId => Guid.NewGuid(); 

    // 正確:僅在 new() 時產生一次
    public Guid CorrectOrderId { get; } = Guid.NewGuid();
}

5. 半自動屬性與 field 關鍵字

什麼情況下會遇到問題:當自動屬性需要在 set 中加入微小邏輯(如 Trim() 或通知變更)時,過去必須退回手寫 Backing Field。

C# 14 引入 field 關鍵字,直接存取編譯器背後的欄位:

csharp
public class User {
    public string Name { 
        get;
        set => field = value.Trim();
    }
}

TIP

建議邏輯處理優先放在 set 中,避免 get 頻繁呼叫帶來的額外開銷,並減少 Entity Framework Core 等框架直接存取 Backing Field 時的潛在問題。


NRT (Nullable Reference Types) 與檢查機制的補全

NRT 旨在透過 ? 標註明確宣告參考型別是否可為空。若要強制執行,可在專案檔設定 <WarningsAsErrors>nullable</WarningsAsErrors>

為什麼以前會想關掉?

在 C# 8.0 至 10.0 時期,DTO 屬性若非 Null,必須提供預設值或使用 null! 欺騙編譯器,這會導致類別定義承擔無法保證的合約。

機制的補全

透過以下語法,NRT 的開發體驗獲得顯著改善:

  • init (C# 9.0):確保屬性僅在初始化期間可賦值,維持不可變性。
  • required (C# 11.0):強制呼叫端在 new() 時必須賦值,無需在類別內寫 null!
csharp
public class UserDto {
    public required string UserName { get; init; }
}

// 呼叫端必須給值,否則編譯失敗
UserDto dto = new() { UserName = "Alice" };

[SetsRequiredMembers]:解決與建構子的衝突

什麼情況下會遇到問題:當類別同時包含自訂建構子與 required 屬性時,編譯器會因無法透過 { } 初始化而發出警告。

csharp
using System.Diagnostics.CodeAnalysis;

public class User {
    public required string UserName { get; init; }

    [SetsRequiredMembers]
    public User(string userName) {
        UserName = userName; 
    }
}

required 在 Web API 的應用

System.Text.Json 序列化中,若屬性標記為 required 但前端漏傳,系統會拋出 JsonException,這有助於區分「前端傳遞預設值」與「前端漏傳」的差異。


異動歷程

    • 初版文件建立。